package org.easystub;

import java.util.List;
import java.util.Arrays;

public class ResultReporter<T> {

    private final InvocationReporter invocationReporter;
    private final RecordedInvocationAnswerer answerer;
    private final ThrowableFactory throwableFactory = ThrowableFactory.instance();

    private static final List<Class<? extends Throwable>> EXCUSED_THROWABLES = Arrays.asList(RuntimeException.class,
                                                                                             Error.class);

    public ResultReporter(InvocationReporter invocationReporter, RecordedInvocationAnswerer answerer) {
        this.invocationReporter = invocationReporter;
        this.answerer = answerer;
    }

    /**
     * Method that takes the answer for the invocation you are stubbing.
     */
    public void withValue(T reply) {
        Invocation invocation = invocationReporter.lastInvocation();
        guardAgainstVoidReturnType(invocation);
        answerer.record(invocation, reply);
    }

    private void guardAgainstVoidReturnType(Invocation invocation) {
        if(Void.TYPE.equals(invocation.getMethodCalled().getReturnType())) {
            throw new UnsupportedOperationException("Cannot register reply on a void method");
        }
    }

    /**
     * Registers an exception as a reply, using the class passed in.
     * EasyStub will try and instantiate the exception for you, using the constructor with the smallest amount
     * of arguments. This works great for Exceptions with no-arg constructors, but for Exceptions with constructors
     * that take arguments, it will try and populate the arguments with
     * default values (false, 0, or null for booleans, number primitives and objects respectively).
     *
     * There are two exceptions to the case above, when the argument type is of or extends
     * java.lang.String or java.lang.Throwable:
     *
     * <ul>
     *  <li>String arguments are filled in with a message about the exception being created by EasyStub.
     *      This is because we assume that this parameter is the message parameter for the exception,
     *      and will be returned when the exception is asked for its message.
     *  </li>
     *  <li>Throwable arguments are filled in with an instance of {@link org.easystub.EasyStubCreatedException}.
     *      This is because we assume that the parameter is the cause parameter.
     *  </li>
     * </ul>
     */
    public void withException(Class<? extends Throwable> throwable) {
        Throwable instance = throwableFactory.create(throwable);
        withException(instance);
    }

    /**
     * Registers an exception as a reply, using the Exception instance passed in.
     */
    public void withException(Throwable throwable) {
        Invocation invocation = invocationReporter.lastInvocation();
        guardAgainstTooGenericThrowable(throwable, invocation);
        answerer.record(invocation, throwable);
    }

    private void guardAgainstTooGenericThrowable(Throwable throwable, Invocation invocation) {
        for(Class<? extends Throwable> excused : EXCUSED_THROWABLES) {
            if(excused.isAssignableFrom(throwable.getClass())) {
                return;
            }
        }
        
        Class<?>[] declaredThrowables = invocation.getMethodCalled().getExceptionTypes();

        for(Class<?> declared : declaredThrowables) {
            if(declared.isAssignableFrom(throwable.getClass())) {
                return;
            }
        }

        throw new UnsupportedOperationException("Cannot throw more generic exception than the one declared on the method.");
    }
}
